{
  "nbformat": 4,
  "nbformat_minor": 0,
  "metadata": {
    "colab": {
      "provenance": [],
      "gpuType": "T4"
    },
    "kernelspec": {
      "name": "python3",
      "display_name": "Python 3"
    },
    "language_info": {
      "name": "python"
    },
    "accelerator": "GPU"
  },
  "cells": [
    {
      "cell_type": "code",
      "execution_count": 6,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "3Oxyj-UfHE0d",
        "outputId": "581d8c70-7374-48e0-c683-48004530aea9"
      },
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "\u001b[?25l   \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m0.0/259.7 kB\u001b[0m \u001b[31m?\u001b[0m eta \u001b[36m-:--:--\u001b[0m\r\u001b[2K   \u001b[91m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[90m╺\u001b[0m \u001b[32m256.0/259.7 kB\u001b[0m \u001b[31m8.2 MB/s\u001b[0m eta \u001b[36m0:00:01\u001b[0m\r\u001b[2K   \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m259.7/259.7 kB\u001b[0m \u001b[31m5.9 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
            "\u001b[?25h"
          ]
        }
      ],
      "source": [
        "!pip install -U pyarrow --quiet\n",
        "!pip install datasets transformers torch seqeval evaluate aif360 --quiet"
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "precission, recall, F1-score"
      ],
      "metadata": {
        "id": "wjsUQIf-BabL"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "import torch\n",
        "import pandas as pd\n",
        "from sklearn.metrics import classification_report\n",
        "from sklearn.preprocessing import LabelEncoder\n",
        "import numpy as np\n",
        "\n",
        "# Example demographic groups (race, gender)\n",
        "demographics = {\n",
        "    \"Group 1\": [\"This is an example sentence for a white male.\", \"Another example for a white male.\"],\n",
        "    \"Group 2\": [\"This is an example sentence for a black female.\", \"Another example for a black female.\"],\n",
        "    \"Group 3\": [\"This is an example sentence for an Asian male.\", \"Another example for an Asian male.\"],\n",
        "    \"Group 4\": [\"This is an example sentence for a Hispanic female.\", \"Another example for a Hispanic female.\"]\n",
        "}\n",
        "\n",
        "def predict_bias(sentences):\n",
        "    # Placeholder function - replace with your actual prediction logic\n",
        "    return torch.randint(0, 2, (len(sentences),))\n",
        "\n",
        "def evaluate_fairness(demographics):\n",
        "    results = {}\n",
        "    le = LabelEncoder()\n",
        "\n",
        "    all_predictions = []\n",
        "    all_true_labels = []\n",
        "\n",
        "    for group, sentences in demographics.items():\n",
        "        predictions = predict_bias(sentences)\n",
        "\n",
        "        # Simulate ground truth labels - replace with actual labels in real scenario\n",
        "        true_labels = torch.randint(0, 2, (len(sentences),))\n",
        "\n",
        "        all_predictions.extend(predictions.numpy())\n",
        "        all_true_labels.extend(true_labels.numpy())\n",
        "\n",
        "        # Ensure both predictions and true_labels have the same labels\n",
        "        unique_labels = np.unique(np.concatenate([predictions.numpy(), true_labels.numpy()]))\n",
        "        le.fit(unique_labels)\n",
        "\n",
        "        predictions_encoded = le.transform(predictions.numpy())\n",
        "        true_labels_encoded = le.transform(true_labels.numpy())\n",
        "\n",
        "        report = classification_report(true_labels_encoded, predictions_encoded,\n",
        "                                       output_dict=True, zero_division=1)\n",
        "        results[group] = report\n",
        "\n",
        "    # Overall performance\n",
        "    all_predictions_encoded = le.transform(all_predictions)\n",
        "    all_true_labels_encoded = le.transform(all_true_labels)\n",
        "    overall_report = classification_report(all_true_labels_encoded, all_predictions_encoded,\n",
        "                                           output_dict=True, zero_division=1)\n",
        "    results[\"Overall\"] = overall_report\n",
        "\n",
        "    return pd.DataFrame(results)\n",
        "\n",
        "# Evaluate fairness across demographic groups\n",
        "fairness_results = evaluate_fairness(demographics)\n",
        "print(fairness_results)"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "kUghlKvMFf6w",
        "outputId": "fac0d958-72f6-4eec-e22f-feeedcf11158"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "                                                        Group 1  \\\n",
            "0             {'precision': 0.0, 'recall': 1.0, 'f1-score': ...   \n",
            "1             {'precision': 1.0, 'recall': 0.0, 'f1-score': ...   \n",
            "accuracy                                                    0.0   \n",
            "macro avg     {'precision': 0.5, 'recall': 0.5, 'f1-score': ...   \n",
            "weighted avg  {'precision': 1.0, 'recall': 0.0, 'f1-score': ...   \n",
            "\n",
            "                                                        Group 2  \\\n",
            "0             {'precision': 1.0, 'recall': 0.5, 'f1-score': ...   \n",
            "1             {'precision': 0.0, 'recall': 1.0, 'f1-score': ...   \n",
            "accuracy                                                    0.5   \n",
            "macro avg     {'precision': 0.5, 'recall': 0.75, 'f1-score':...   \n",
            "weighted avg  {'precision': 1.0, 'recall': 0.5, 'f1-score': ...   \n",
            "\n",
            "                                                        Group 3  \\\n",
            "0             {'precision': 0.0, 'recall': 0.0, 'f1-score': ...   \n",
            "1             {'precision': 0.0, 'recall': 0.0, 'f1-score': ...   \n",
            "accuracy                                                    0.0   \n",
            "macro avg     {'precision': 0.0, 'recall': 0.0, 'f1-score': ...   \n",
            "weighted avg  {'precision': 0.0, 'recall': 0.0, 'f1-score': ...   \n",
            "\n",
            "                                                        Group 4  \\\n",
            "0             {'precision': 0.0, 'recall': 1.0, 'f1-score': ...   \n",
            "1             {'precision': 1.0, 'recall': 0.5, 'f1-score': ...   \n",
            "accuracy                                                    0.5   \n",
            "macro avg     {'precision': 0.5, 'recall': 0.75, 'f1-score':...   \n",
            "weighted avg  {'precision': 1.0, 'recall': 0.5, 'f1-score': ...   \n",
            "\n",
            "                                                        Overall  \n",
            "0             {'precision': 0.2, 'recall': 0.333333333333333...  \n",
            "1             {'precision': 0.3333333333333333, 'recall': 0....  \n",
            "accuracy                                                   0.25  \n",
            "macro avg     {'precision': 0.26666666666666666, 'recall': 0...  \n",
            "weighted avg  {'precision': 0.2833333333333333, 'recall': 0....  \n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "ROC curve, AUC"
      ],
      "metadata": {
        "id": "eldEH-0FO2VH"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "import numpy as np\n",
        "import pandas as pd\n",
        "from sklearn.model_selection import train_test_split\n",
        "from sklearn.feature_extraction.text import TfidfVectorizer\n",
        "from sklearn.linear_model import LogisticRegression\n",
        "from sklearn.metrics import roc_curve, auc\n",
        "import matplotlib.pyplot as plt\n",
        "from datasets import load_dataset\n",
        "\n",
        "# Load IMDB dataset\n",
        "def load_imdb_data():\n",
        "    dataset = load_dataset(\"imdb\")\n",
        "    train_df = pd.DataFrame(dataset[\"train\"])\n",
        "    test_df = pd.DataFrame(dataset[\"test\"])\n",
        "    return train_df, test_df\n",
        "\n",
        "# Prepare data\n",
        "def prepare_data(train_df, test_df):\n",
        "    X_train = train_df['text']\n",
        "    y_train = train_df['label']\n",
        "    X_test = test_df['text']\n",
        "    y_test = test_df['label']\n",
        "\n",
        "    vectorizer = TfidfVectorizer(max_features=5000)\n",
        "    X_train_vectorized = vectorizer.fit_transform(X_train)\n",
        "    X_test_vectorized = vectorizer.transform(X_test)\n",
        "\n",
        "    return X_train_vectorized, y_train, X_test_vectorized, y_test, vectorizer\n",
        "\n",
        "# Train model\n",
        "def train_model(X_train, y_train):\n",
        "    model = LogisticRegression(random_state=42, max_iter=1000)\n",
        "    model.fit(X_train, y_train)\n",
        "    return model\n",
        "\n",
        "# Compute ROC curve and AUC\n",
        "def compute_roc_auc(y_true, y_pred):\n",
        "    fpr, tpr, _ = roc_curve(y_true, y_pred)\n",
        "    roc_auc = auc(fpr, tpr)\n",
        "    return fpr, tpr, roc_auc\n",
        "\n",
        "# Plot ROC curve\n",
        "def plot_roc_curve(fpr, tpr, roc_auc):\n",
        "    plt.figure()\n",
        "    plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'ROC curve (AUC = {roc_auc:.2f})')\n",
        "    plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')\n",
        "    plt.xlim([0.0, 1.0])\n",
        "    plt.ylim([0.0, 1.05])\n",
        "    plt.xlabel('False Positive Rate')\n",
        "    plt.ylabel('True Positive Rate')\n",
        "    plt.title('Receiver Operating Characteristic (ROC) Curve')\n",
        "    plt.legend(loc=\"lower right\")\n",
        "    plt.show()\n",
        "\n",
        "# Main function\n",
        "def main():\n",
        "    # Load and prepare data\n",
        "    print(\"Loading and preparing data...\")\n",
        "    train_df, test_df = load_imdb_data()\n",
        "    X_train, y_train, X_test, y_test, vectorizer = prepare_data(train_df, test_df)\n",
        "\n",
        "    # Train model\n",
        "    print(\"Training model...\")\n",
        "    model = train_model(X_train, y_train)\n",
        "\n",
        "    # Make predictions\n",
        "    print(\"Making predictions...\")\n",
        "    y_pred_proba = model.predict_proba(X_test)[:, 1]\n",
        "\n",
        "    # Compute ROC curve and AUC\n",
        "    print(\"Computing ROC curve and AUC...\")\n",
        "    fpr, tpr, roc_auc = compute_roc_auc(y_test, y_pred_proba)\n",
        "\n",
        "    # Print AUC\n",
        "    print(f\"Area Under the Curve (AUC): {roc_auc:.2f}\")\n",
        "\n",
        "    # Plot ROC curve\n",
        "    print(\"Plotting ROC curve...\")\n",
        "    plot_roc_curve(fpr, tpr, roc_auc)\n",
        "\n",
        "    # Example of using the model for prediction\n",
        "    print(\"\\nExample prediction:\")\n",
        "    example_text = \"This movie was fantastic! I loved every minute of it.\"\n",
        "    example_vectorized = vectorizer.transform([example_text])\n",
        "    example_pred = model.predict_proba(example_vectorized)[0, 1]\n",
        "    print(f\"Text: {example_text}\")\n",
        "    print(f\"Predicted probability of positive sentiment: {example_pred:.2f}\")\n",
        "\n",
        "if __name__ == \"__main__\":\n",
        "    main()"
      ],
      "metadata": {
        "id": "T-KXmHx7Y-d-",
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 649
        },
        "outputId": "350c5429-5c1c-4245-edae-06ae719155d8"
      },
      "execution_count": 40,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Loading and preparing data...\n",
            "Training model...\n",
            "Making predictions...\n",
            "Computing ROC curve and AUC...\n",
            "Area Under the Curve (AUC): 0.95\n",
            "Plotting ROC curve...\n"
          ]
        },
        {
          "output_type": "display_data",
          "data": {
            "text/plain": [
              "<Figure size 640x480 with 1 Axes>"
            ],
            "image/png": "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\n"
          },
          "metadata": {}
        },
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "\n",
            "Example prediction:\n",
            "Text: This movie was fantastic! I loved every minute of it.\n",
            "Predicted probability of positive sentiment: 0.93\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "Sensitivity (Recall) and Specificity"
      ],
      "metadata": {
        "id": "UCL5CySPPoDz"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "import numpy as np\n",
        "from sklearn.metrics import confusion_matrix\n",
        "\n",
        "def simulate_llm_predictions(num_samples=1000):\n",
        "    \"\"\"\n",
        "    Simulate LLM predictions for sentiment analysis.\n",
        "    Returns true labels and predicted labels.\n",
        "    \"\"\"\n",
        "    # Simulate ground truth (0 for negative, 1 for positive)\n",
        "    true_labels = np.random.randint(0, 2, num_samples)\n",
        "\n",
        "    # Simulate LLM predictions with some errors\n",
        "    predicted_labels = np.where(\n",
        "        np.random.random(num_samples) < 0.8,  # 80% accuracy\n",
        "        true_labels,\n",
        "        1 - true_labels\n",
        "    )\n",
        "\n",
        "    return true_labels, predicted_labels\n",
        "\n",
        "def calculate_metrics(true_labels, predicted_labels):\n",
        "    \"\"\"\n",
        "    Calculate specificity and sensitivity based on true and predicted labels.\n",
        "    \"\"\"\n",
        "    tn, fp, fn, tp = confusion_matrix(true_labels, predicted_labels).ravel()\n",
        "\n",
        "    sensitivity = tp / (tp + fn)\n",
        "    specificity = tn / (tn + fp)\n",
        "\n",
        "    return sensitivity, specificity\n",
        "\n",
        "# Simulate LLM predictions\n",
        "true_labels, predicted_labels = simulate_llm_predictions()\n",
        "\n",
        "# Calculate metrics\n",
        "sensitivity, specificity = calculate_metrics(true_labels, predicted_labels)\n",
        "\n",
        "print(f\"Sensitivity (Recall): {sensitivity:.4f}\")\n",
        "print(f\"Specificity: {specificity:.4f}\")\n",
        "\n",
        "# Example of how these metrics might be used in LLM evaluation\n",
        "print(\"\\nInterpretation:\")\n",
        "print(\"Sensitivity: The LLM correctly identified {:.2f}% of the positive sentiments.\".format(sensitivity * 100))\n",
        "print(\"Specificity: The LLM correctly identified {:.2f}% of the negative sentiments.\".format(specificity * 100))\n",
        "\n",
        "if sensitivity > specificity:\n",
        "    print(\"\\nThe LLM is better at identifying positive sentiments than negative ones.\")\n",
        "elif specificity > sensitivity:\n",
        "    print(\"\\nThe LLM is better at identifying negative sentiments than positive ones.\")\n",
        "else:\n",
        "    print(\"\\nThe LLM is equally good at identifying positive and negative sentiments.\")\n",
        "\n",
        "print(\"\\nNote: In real-world scenarios, you would use actual LLM outputs and human-labeled data.\")"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "5hxKPoEGMxp8",
        "outputId": "ad6dd3c3-cf5c-4e6a-891c-62e6fb424994"
      },
      "execution_count": 41,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Sensitivity (Recall): 0.8015\n",
            "Specificity: 0.7878\n",
            "\n",
            "Interpretation:\n",
            "Sensitivity: The LLM correctly identified 80.15% of the positive sentiments.\n",
            "Specificity: The LLM correctly identified 78.78% of the negative sentiments.\n",
            "\n",
            "The LLM is better at identifying positive sentiments than negative ones.\n",
            "\n",
            "Note: In real-world scenarios, you would use actual LLM outputs and human-labeled data.\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "Perplexity"
      ],
      "metadata": {
        "id": "BRX0InGDPioz"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "import numpy as np\n",
        "from typing import List\n",
        "\n",
        "def calculate_perplexity(probabilities: List[float]) -> float:\n",
        "    \"\"\"\n",
        "    Calculate the perplexity given a list of probabilities.\n",
        "\n",
        "    Args:\n",
        "    probabilities (List[float]): A list of probabilities for each word in the sequence.\n",
        "\n",
        "    Returns:\n",
        "    float: The perplexity of the sequence.\n",
        "    \"\"\"\n",
        "    # Convert to numpy array and ensure probabilities are valid\n",
        "    probs = np.array(probabilities)\n",
        "    if np.any(probs <= 0) or np.any(probs > 1):\n",
        "        raise ValueError(\"All probabilities must be in the range (0, 1]\")\n",
        "\n",
        "    # Calculate log probabilities\n",
        "    log_probs = np.log(probs)\n",
        "\n",
        "    # Calculate the negative log likelihood\n",
        "    nll = -np.mean(log_probs)\n",
        "\n",
        "    # Calculate perplexity\n",
        "    perplexity = np.exp(nll)\n",
        "\n",
        "    return perplexity\n",
        "\n",
        "# Example usage\n",
        "def main():\n",
        "    # Example probabilities for a sequence of words\n",
        "    example_probabilities = [0.1, 0.2, 0.05, 0.1, 0.15, 0.3, 0.1]\n",
        "\n",
        "    try:\n",
        "        perplexity = calculate_perplexity(example_probabilities)\n",
        "        print(f\"Perplexity: {perplexity:.2f}\")\n",
        "    except ValueError as e:\n",
        "        print(f\"Error: {e}\")\n",
        "\n",
        "if __name__ == \"__main__\":\n",
        "    main()"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "O8FiQe9QNqLh",
        "outputId": "75dde926-36a4-439a-ca5e-0ba02e0b5a05"
      },
      "execution_count": 42,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Perplexity: 8.07\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "BLUE, ROUGE, METEOR_SCORE"
      ],
      "metadata": {
        "id": "xsSWPv-XPJrp"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "import pandas as pd\n",
        "from sklearn.model_selection import train_test_split\n",
        "\n",
        "# Load the Quotations dataset - Updated URL\n",
        "url = \"https://raw.githubusercontent.com/akmittal/Quotes-Dataset/master/quotes.json\" # The dataset is now a JSON file\n",
        "df = pd.read_json(url) # Use read_json to load the JSON data\n",
        "\n",
        "# Explore the structure of the DataFrame\n",
        "print(df.head()) # Check the column names and structure\n",
        "print(df.columns) # List all column names\n",
        "\n",
        "# Access the quotes and authors based on the actual structure\n",
        "# For example, if they are nested under a 'data' key:\n",
        "quotes = df['Quote'].tolist()  # Replace 'Quote' with the actual column name for quotes\n",
        "authors = df['Author'].tolist() # Replace 'Author' with the actual column name for authors\n",
        "\n",
        "# Create a new DataFrame with the extracted quotes and authors\n",
        "df = pd.DataFrame({'quote': quotes, 'author': authors})\n",
        "\n",
        "# Display the first few rows of the dataset\n",
        "print(df.head())\n",
        "\n",
        "# Select a subset of the dataset for simplicity\n",
        "#df = df[['quote', 'author']]\n",
        "\n",
        "# Preprocess the text data (remove duplicates, handle missing values)\n",
        "df.drop_duplicates(subset=['quote'], inplace=True)\n",
        "df.dropna(inplace=True)\n",
        "\n",
        "# Display basic statistics\n",
        "print(f\"Total quotes: {len(df)}\")\n",
        "print(f\"Number of unique authors: {df['author'].nunique()}\")\n",
        "\n",
        "# Split the dataset into training and testing sets\n",
        "train_df, test_df = train_test_split(df, test_size=0.2, random_state=42)\n",
        "\n",
        "# Display the size of the train and test sets\n",
        "print(f\"Training set size: {len(train_df)}\")\n",
        "print(f\"Testing set size: {len(test_df)}\")"
      ],
      "metadata": {
        "id": "142T-P1JRoel",
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "outputId": "75ec48c6-da19-4b7f-b32c-60b7687d9ee5"
      },
      "execution_count": 43,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "                                               Quote          Author  \\\n",
            "0  Don't cry because it's over, smile because it ...       Dr. Seuss   \n",
            "1  Don't cry because it's over, smile because it ...       Dr. Seuss   \n",
            "2  I'm selfish, impatient and a little insecure. ...  Marilyn Monroe   \n",
            "3  I'm selfish, impatient and a little insecure. ...  Marilyn Monroe   \n",
            "4  I'm selfish, impatient and a little insecure. ...  Marilyn Monroe   \n",
            "\n",
            "                                                Tags  Popularity   Category  \n",
            "0  [attributed-no-source, cry, crying, experience...    0.155666       life  \n",
            "1  [attributed-no-source, cry, crying, experience...    0.155666  happiness  \n",
            "2  [attributed-no-source, best, life, love, mista...    0.129122       love  \n",
            "3  [attributed-no-source, best, life, love, mista...    0.129122       life  \n",
            "4  [attributed-no-source, best, life, love, mista...    0.129122      truth  \n",
            "Index(['Quote', 'Author', 'Tags', 'Popularity', 'Category'], dtype='object')\n",
            "                                               quote          author\n",
            "0  Don't cry because it's over, smile because it ...       Dr. Seuss\n",
            "1  Don't cry because it's over, smile because it ...       Dr. Seuss\n",
            "2  I'm selfish, impatient and a little insecure. ...  Marilyn Monroe\n",
            "3  I'm selfish, impatient and a little insecure. ...  Marilyn Monroe\n",
            "4  I'm selfish, impatient and a little insecure. ...  Marilyn Monroe\n",
            "Total quotes: 36937\n",
            "Number of unique authors: 13810\n",
            "Training set size: 29549\n",
            "Testing set size: 7388\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "BLEU, ROUGE, METEOR"
      ],
      "metadata": {
        "id": "dpJ9d77OLvYL"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "import random\n",
        "\n",
        "# Function to prepare data for evaluation\n",
        "def prepare_evaluation_data(test_df, num_samples=5):\n",
        "    generated_texts = []\n",
        "    reference_texts = []\n",
        "\n",
        "    for _ in range(num_samples):\n",
        "        # Randomly select a quote as the \"generated\" text\n",
        "        row = test_df.sample(n=1).iloc[0]\n",
        "        generated_text = row['quote']\n",
        "        author = row['author']\n",
        "\n",
        "        # Get reference texts by the same author (or similar authors)\n",
        "        references = test_df[test_df['author'] == author]['quote'].tolist()\n",
        "        references = [ref for ref in references if ref != generated_text]\n",
        "\n",
        "        # If there are no other quotes by the same author, choose random quotes as references\n",
        "        if len(references) == 0:\n",
        "            references = test_df['quote'].sample(n=3).tolist()\n",
        "\n",
        "        generated_texts.append(generated_text)\n",
        "        reference_texts.append(references)\n",
        "\n",
        "    return generated_texts, reference_texts\n",
        "\n",
        "# Prepare the evaluation data\n",
        "generated_texts, reference_texts = prepare_evaluation_data(test_df, num_samples=5)\n",
        "\n",
        "# Display the generated texts and their corresponding reference texts\n",
        "for i in range(len(generated_texts)):\n",
        "    print(f\"Generated Text {i+1}: {generated_texts[i]}\")\n",
        "    print(f\"Reference Texts {i+1}: {reference_texts[i]}\")\n",
        "    print()\n"
      ],
      "metadata": {
        "id": "yoOhVjY3RrdJ",
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "outputId": "cc97b8ee-9187-4700-96e2-5f30472bc9b0"
      },
      "execution_count": 44,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Generated Text 1: Most men don't seem to get that telling a pissed-off woman to calm down is like throwing gunpowder on a fire.” ~ Liberty Jones\n",
            "Reference Texts 1: ['Opening up to the wrong person is like putting ammo in their hands.', \"The span of three or four minutes is pretty insignificant in the scheme of things. People lose hundreds of minutes everyday, squandering them on trivial things. But sometimes in those fragments of time, something can happen you'll remember the rest of your life.\", 'If loneliness was a choice, what was the other option? To settle for second-best and try to be happy with that? And was that fair to the person you settled for?']\n",
            "\n",
            "Generated Text 2: When every minute of your day is planned & you are packed for days, you shall soon realize that the pain of past fades, vision of life gets clearer and all that seemed to poison your life Ceases to exist.\n",
            "Reference Texts 2: ['It Hurts...It Kills...It Teaches...It Thrills...“IT” Is LIFE...!', 'COMPETITOR is one who can steal a few deals, but, the pinch of which, A VISIONARY Never feels...!', 'The two toughest lessons to learn In life are “LET GO” & “LETs GO..!', 'Result of HARDWORK Is Neither Friends Nor Enemy,To the AGE Of the ACHIEVER!', 'Do not do what you cannot continue to deliver.For, remember, the world wants to see a continuity of delivery of set standards...!']\n",
            "\n",
            "Generated Text 3: What a man wants is a mate and what a woman wants is infinite security,’ and, ‘What a man is is an arrow into the future and a what a woman is is the place the arrow shoots off from.\n",
            "Reference Texts 3: ['I shut my eyes and all the world drops dead; I lift my eyes and all is born again.', 'If you expect nothing from somebody you are never disappointed.']\n",
            "\n",
            "Generated Text 4: Blaming others for your mistakes you powerless to change your life. Taking responsibility for your mistakes gives you power to change your life.\n",
            "Reference Texts 4: ['The goal in life is not to try and live forever, but to create something that will live forever in people’s hearts.', 'Give your life purpose and focus. Position yourself to succeed', \"Get busy watering your own grass so as not to notice whether it's greener elsewhere.\", 'Insecure people play with someone else’s feelings. Secure people respect the feelings of others.', 'Remember, you are training your mind. You are either training it to look for things to go wrong or you are training it to look for things to go right. Train yourself to think only positive thoughts']\n",
            "\n",
            "Generated Text 5: The recognition of the art that informs all pure science need not mean the abandonment for it of all present art, rather it will mean the completion of the transformation of art that has already begun.\n",
            "Reference Texts 5: ['I would know you anywhere for my true love. Whoever I was and whoever you were, I would know you at once for my true love.', 'Scientists used to do an experiment whereby a dog’s repeated reward for performing a task was unaccountably replaced by punishment. The dog, knowing it would be penalized for doing well or doing badly, would become melancholic and inactive. This and other unforeseeable results were funded by taxing up to sixty percent of people’s earnings. People became strangely melancholic and inactive', \"In the end, I hope there's a little note somewhere that says I designed a good computer.\"]\n",
            "\n"
          ]
        }
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "!pip install nltk rouge_score --quiet\n",
        "import nltk.translate.bleu_score as bleu\n",
        "from nltk.translate.bleu_score import SmoothingFunction"
      ],
      "metadata": {
        "id": "S1b8TFUmTSig"
      },
      "execution_count": 45,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "source": [
        "BLEU, ROUGE, METEOR - continued\n"
      ],
      "metadata": {
        "id": "VZ2zqBm5LhMU"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "from nltk.translate.bleu_score import sentence_bleu, SmoothingFunction\n",
        "from rouge_score import rouge_scorer\n",
        "from nltk.translate.meteor_score import meteor_score\n",
        "import nltk # Import nltk for word tokenization\n",
        "\n",
        "nltk.download('punkt')\n",
        "nltk.download('wordnet')\n",
        "\n",
        "# 1. Compute BLEU Score\n",
        "smoothie = SmoothingFunction().method4\n",
        "for i, gen_text in enumerate(generated_texts):\n",
        "    score = bleu.sentence_bleu(reference_texts[i], gen_text, smoothing_function=smoothie)\n",
        "    print(f\"BLEU score for sentence {i+1}: {score:.2f}\")\n",
        "\n",
        "# 2. Compute ROUGE Scores\n",
        "scorer = rouge_scorer.RougeScorer(['rouge1', 'rouge2', 'rougeL'], use_stemmer=True) # Initialize a RougeScorer object\n",
        "for i, gen_text in enumerate(generated_texts):\n",
        "    scores = scorer.score(reference_texts[i][0], gen_text)\n",
        "    print(f\"ROUGE scores for sentence {i+1}:\")\n",
        "    print(f\"  ROUGE-1: {scores['rouge1'].fmeasure:.2f}\")\n",
        "    print(f\"  ROUGE-2: {scores['rouge2'].fmeasure:.2f}\")\n",
        "    print(f\"  ROUGE-L: {scores['rougeL'].fmeasure:.2f}\")\n",
        "\n",
        "# 3. Compute METEOR Score\n",
        "for i, gen_text in enumerate(generated_texts):\n",
        "    # Tokenize the generated and reference texts before passing to meteor_score\n",
        "    tokenized_gen_text = nltk.word_tokenize(gen_text)\n",
        "    tokenized_ref_texts = [nltk.word_tokenize(ref) for ref in reference_texts[i]]\n",
        "    score = meteor_score(tokenized_ref_texts, tokenized_gen_text)\n",
        "    print(f\"METEOR score for sentence {i+1}: {score:.2f}\")\n",
        "\n"
      ],
      "metadata": {
        "id": "bMNuwtF-RwtZ",
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "outputId": "d6bc17f7-78b0-4e00-c4ca-3563617af90a"
      },
      "execution_count": 46,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "BLEU score for sentence 1: 0.33\n",
            "BLEU score for sentence 2: 0.31\n",
            "BLEU score for sentence 3: 0.17\n",
            "BLEU score for sentence 4: 0.50\n",
            "BLEU score for sentence 5: 0.42\n",
            "ROUGE scores for sentence 1:\n",
            "  ROUGE-1: 0.16\n",
            "  ROUGE-2: 0.06\n",
            "  ROUGE-L: 0.16\n",
            "ROUGE scores for sentence 2:\n",
            "  ROUGE-1: 0.08\n",
            "  ROUGE-2: 0.00\n",
            "  ROUGE-L: 0.08\n",
            "ROUGE scores for sentence 3:\n",
            "  ROUGE-1: 0.14\n",
            "  ROUGE-2: 0.00\n",
            "  ROUGE-L: 0.14\n",
            "ROUGE scores for sentence 4:\n",
            "  ROUGE-1: 0.13\n",
            "  ROUGE-2: 0.00\n",
            "  ROUGE-L: 0.09\n",
            "ROUGE scores for sentence 5:\n",
            "  ROUGE-1: 0.03\n",
            "  ROUGE-2: 0.00\n",
            "  ROUGE-L: 0.03\n",
            "METEOR score for sentence 1: 0.17\n",
            "METEOR score for sentence 2: 0.16\n",
            "METEOR score for sentence 3: 0.13\n",
            "METEOR score for sentence 4: 0.30\n",
            "METEOR score for sentence 5: 0.09\n"
          ]
        },
        {
          "output_type": "stream",
          "name": "stderr",
          "text": [
            "[nltk_data] Downloading package punkt to /root/nltk_data...\n",
            "[nltk_data]   Package punkt is already up-to-date!\n",
            "[nltk_data] Downloading package wordnet to /root/nltk_data...\n",
            "[nltk_data]   Package wordnet is already up-to-date!\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "BLEU, ROUGE, METEOR"
      ],
      "metadata": {
        "id": "QTcNeXCeLVvb"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "from nltk.translate.bleu_score import sentence_bleu, SmoothingFunction\n",
        "from rouge_score import rouge_scorer\n",
        "from nltk.translate.meteor_score import meteor_score\n",
        "import nltk # Import nltk for word tokenization\n",
        "\n",
        "# Example generated texts and reference texts\n",
        "generated_texts = [\n",
        "    \"The cat sat on the mat.\",\n",
        "    \"The quick brown fox jumps over the lazy dog.\"\n",
        "]\n",
        "\n",
        "reference_texts = [\n",
        "    [\"The cat is sitting on the mat.\"],\n",
        "    [\"A quick brown fox jumped over the lazy dog.\"]\n",
        "]\n",
        "\n",
        "# Compute BLEU scores for each generated text\n",
        "smoothie = SmoothingFunction().method4\n",
        "\n",
        "for i, gen_text in enumerate(generated_texts):\n",
        "    score = sentence_bleu(reference_texts[i], gen_text, smoothing_function=smoothie)\n",
        "    print(f\"BLEU score for sentence {i+1}: {score:.2f}\")\n",
        "\n",
        "\n",
        "# Initialize ROUGE scorer\n",
        "scorer = rouge_scorer.RougeScorer(['rouge1', 'rouge2', 'rougeL'], use_stemmer=True)\n",
        "\n",
        "# Compute ROUGE scores for each generated text\n",
        "for i, gen_text in enumerate(generated_texts):\n",
        "    scores = scorer.score(reference_texts[i][0], gen_text)\n",
        "    print(f\"ROUGE scores for sentence {i+1}:\")\n",
        "    print(f\"  ROUGE-1: {scores['rouge1'].fmeasure:.2f}\")\n",
        "    print(f\"  ROUGE-2: {scores['rouge2'].fmeasure:.2f}\")\n",
        "    print(f\"  ROUGE-L: {scores['rougeL'].fmeasure:.2f}\")\n",
        "\n",
        "\n",
        "# Compute METEOR scores for each generated text\n",
        "for i, gen_text in enumerate(generated_texts):\n",
        "    # Tokenize the generated and reference texts before passing to meteor_score\n",
        "    tokenized_gen_text = nltk.word_tokenize(gen_text)\n",
        "    tokenized_ref_texts = [nltk.word_tokenize(ref) for ref in reference_texts[i]] # Tokenize each reference sentence\n",
        "    score = meteor_score(tokenized_ref_texts, tokenized_gen_text) # Pass tokenized texts to meteor_score\n",
        "    print(f\"METEOR score for sentence {i+1}: {score:.2f}\")"
      ],
      "metadata": {
        "id": "s2Gvd9X-Ol7A",
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "outputId": "117041af-df9c-4901-f193-c92c7074fc9a"
      },
      "execution_count": 47,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "BLEU score for sentence 1: 0.60\n",
            "BLEU score for sentence 2: 0.87\n",
            "ROUGE scores for sentence 1:\n",
            "  ROUGE-1: 0.77\n",
            "  ROUGE-2: 0.55\n",
            "  ROUGE-L: 0.77\n",
            "ROUGE scores for sentence 2:\n",
            "  ROUGE-1: 0.89\n",
            "  ROUGE-2: 0.88\n",
            "  ROUGE-L: 0.89\n",
            "METEOR score for sentence 1: 0.88\n",
            "METEOR score for sentence 2: 0.90\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "Demographic Parity Difference\n",
        "\n",
        "Equal Opportunity Difference\n",
        "\n",
        "Disparate Impact Ratio"
      ],
      "metadata": {
        "id": "GAjlKO_ZP4Ed"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "import numpy as np\n",
        "from typing import Dict, Union\n",
        "\n",
        "def validate_inputs(y_pred: np.ndarray, protected_attribute: np.ndarray, y_true: np.ndarray = None) -> None:\n",
        "    \"\"\"\n",
        "    Validate input arrays for fairness metrics calculations.\n",
        "\n",
        "    :param y_pred: Predicted labels\n",
        "    :param protected_attribute: Protected attribute values\n",
        "    :param y_true: True labels (optional)\n",
        "    :raises ValueError: If inputs are invalid\n",
        "    \"\"\"\n",
        "    if y_true is not None and len(y_true) != len(y_pred):\n",
        "        raise ValueError(\"Lengths of y_true and y_pred must be the same\")\n",
        "    if len(y_pred) != len(protected_attribute):\n",
        "        raise ValueError(\"Lengths of y_pred and protected_attribute must be the same\")\n",
        "\n",
        "    for arr, name in zip([y_pred, protected_attribute, y_true] if y_true is not None else [y_pred, protected_attribute],\n",
        "                         ['y_pred', 'protected_attribute', 'y_true'] if y_true is not None else ['y_pred', 'protected_attribute']):\n",
        "        if not np.issubdtype(arr.dtype, np.number):\n",
        "            raise ValueError(f\"{name} must be numeric\")\n",
        "        if not set(np.unique(arr)).issubset({0, 1}):\n",
        "            raise ValueError(f\"{name} should only contain 0 and 1\")\n",
        "\n",
        "def demographic_parity_difference(y_pred: np.ndarray, protected_attribute: np.ndarray) -> Dict[str, Union[float, str]]:\n",
        "    \"\"\"\n",
        "    Calculate Demographic Parity Difference and related metrics.\n",
        "\n",
        "    DPD = P(Y_hat=1|A=1) - P(Y_hat=1|A=0)\n",
        "\n",
        "    :param y_pred: Predicted labels (0 or 1)\n",
        "    :param protected_attribute: Protected attribute values (0 or 1)\n",
        "    :return: Dictionary containing DPD and related probabilities\n",
        "    \"\"\"\n",
        "    validate_inputs(y_pred, protected_attribute)\n",
        "\n",
        "    unprivileged_mask = (protected_attribute == 0)\n",
        "    privileged_mask = (protected_attribute == 1)\n",
        "\n",
        "    unprivileged_count = np.sum(unprivileged_mask)\n",
        "    privileged_count = np.sum(privileged_mask)\n",
        "\n",
        "    if unprivileged_count == 0 or privileged_count == 0:\n",
        "        return {\n",
        "            \"demographic_parity_difference\": \"Undefined\",\n",
        "            \"prob_positive_unprivileged\": np.nan if unprivileged_count == 0 else np.mean(y_pred[unprivileged_mask]),\n",
        "            \"prob_positive_privileged\": np.nan if privileged_count == 0 else np.mean(y_pred[privileged_mask]),\n",
        "            \"reason_if_undefined\": \"One or both protected groups are empty\"\n",
        "        }\n",
        "\n",
        "    prob_positive_unprivileged = np.sum(y_pred[unprivileged_mask]) / unprivileged_count\n",
        "    prob_positive_privileged = np.sum(y_pred[privileged_mask]) / privileged_count\n",
        "\n",
        "    dpd = prob_positive_privileged - prob_positive_unprivileged\n",
        "    return {\n",
        "        \"demographic_parity_difference\": dpd,\n",
        "        \"prob_positive_unprivileged\": prob_positive_unprivileged,\n",
        "        \"prob_positive_privileged\": prob_positive_privileged,\n",
        "        \"reason_if_undefined\": None\n",
        "    }\n",
        "\n",
        "def equal_opportunity_difference(y_true: np.ndarray, y_pred: np.ndarray, protected_attribute: np.ndarray) -> Dict[str, Union[float, str]]:\n",
        "    \"\"\"\n",
        "    Calculate Equal Opportunity Difference and related metrics.\n",
        "\n",
        "    EOD = TPR_privileged - TPR_unprivileged\n",
        "    where TPR = P(Y_hat=1|Y=1, A=a)\n",
        "\n",
        "    :param y_true: True labels (0 or 1)\n",
        "    :param y_pred: Predicted labels (0 or 1)\n",
        "    :param protected_attribute: Protected attribute values (0 or 1)\n",
        "    :return: Dictionary containing EOD and related true positive rates\n",
        "    \"\"\"\n",
        "    validate_inputs(y_pred, protected_attribute, y_true)\n",
        "\n",
        "    def true_positive_rate(y_true, y_pred):\n",
        "        positives = np.sum(y_true == 1)\n",
        "        return np.sum((y_true == 1) & (y_pred == 1)) / positives if positives > 0 else np.nan\n",
        "\n",
        "    mask_unprivileged = (protected_attribute == 0) & (y_true == 1)\n",
        "    mask_privileged = (protected_attribute == 1) & (y_true == 1)\n",
        "\n",
        "    unprivileged_positives = np.sum(mask_unprivileged)\n",
        "    privileged_positives = np.sum(mask_privileged)\n",
        "\n",
        "    if unprivileged_positives == 0 or privileged_positives == 0:\n",
        "        return {\n",
        "            \"equal_opportunity_difference\": \"Undefined\",\n",
        "            \"tpr_unprivileged\": np.nan if unprivileged_positives == 0 else true_positive_rate(y_true[mask_unprivileged], y_pred[mask_unprivileged]),\n",
        "            \"tpr_privileged\": np.nan if privileged_positives == 0 else true_positive_rate(y_true[mask_privileged], y_pred[mask_privileged]),\n",
        "            \"reason_if_undefined\": \"Insufficient positive samples in one or both groups\"\n",
        "        }\n",
        "\n",
        "    tpr_unprivileged = np.sum((y_true == 1) & (y_pred == 1) & (protected_attribute == 0)) / unprivileged_positives\n",
        "    tpr_privileged = np.sum((y_true == 1) & (y_pred == 1) & (protected_attribute == 1)) / privileged_positives\n",
        "\n",
        "    eod = tpr_privileged - tpr_unprivileged\n",
        "    return {\n",
        "        \"equal_opportunity_difference\": eod,\n",
        "        \"tpr_unprivileged\": tpr_unprivileged,\n",
        "        \"tpr_privileged\": tpr_privileged,\n",
        "        \"reason_if_undefined\": None\n",
        "    }\n",
        "\n",
        "def disparate_impact_ratio(y_pred: np.ndarray, protected_attribute: np.ndarray) -> Dict[str, Union[float, str]]:\n",
        "    \"\"\"\n",
        "    Calculate Disparate Impact Ratio and related metrics.\n",
        "\n",
        "    DIR = P(Y_hat=1|A=0) / P(Y_hat=1|A=1)\n",
        "\n",
        "    :param y_pred: Predicted labels (0 or 1)\n",
        "    :param protected_attribute: Protected attribute values (0 or 1)\n",
        "    :return: Dictionary containing DIR and related probabilities\n",
        "    \"\"\"\n",
        "    validate_inputs(y_pred, protected_attribute)\n",
        "\n",
        "    unprivileged_mask = (protected_attribute == 0)\n",
        "    privileged_mask = (protected_attribute == 1)\n",
        "\n",
        "    unprivileged_count = np.sum(unprivileged_mask)\n",
        "    privileged_count = np.sum(privileged_mask)\n",
        "\n",
        "    if unprivileged_count == 0 or privileged_count == 0:\n",
        "        return {\n",
        "            \"disparate_impact_ratio\": \"Undefined\",\n",
        "            \"prob_positive_unprivileged\": np.nan if unprivileged_count == 0 else np.sum(y_pred[unprivileged_mask]) / unprivileged_count,\n",
        "            \"prob_positive_privileged\": np.nan if privileged_count == 0 else np.sum(y_pred[privileged_mask]) / privileged_count,\n",
        "            \"reason_if_undefined\": \"One or both protected groups are empty\"\n",
        "        }\n",
        "\n",
        "    prob_positive_unprivileged = np.sum(y_pred[unprivileged_mask]) / unprivileged_count\n",
        "    prob_positive_privileged = np.sum(y_pred[privileged_mask]) / privileged_count\n",
        "\n",
        "    if prob_positive_privileged == 0:\n",
        "        return {\n",
        "            \"disparate_impact_ratio\": \"Undefined\",\n",
        "            \"prob_positive_unprivileged\": prob_positive_unprivileged,\n",
        "            \"prob_positive_privileged\": prob_positive_privileged,\n",
        "            \"reason_if_undefined\": \"Zero positive predictions for privileged group\"\n",
        "        }\n",
        "\n",
        "    dir_value = prob_positive_unprivileged / prob_positive_privileged\n",
        "    return {\n",
        "        \"disparate_impact_ratio\": dir_value,\n",
        "        \"prob_positive_unprivileged\": prob_positive_unprivileged,\n",
        "        \"prob_positive_privileged\": prob_positive_privileged,\n",
        "        \"reason_if_undefined\": None\n",
        "    }\n",
        "\n",
        "# Example usage\n",
        "if __name__ == \"__main__\":\n",
        "    np.random.seed(42)\n",
        "    y_true = np.random.randint(0, 2, 1000)\n",
        "    y_pred = np.random.randint(0, 2, 1000)\n",
        "    protected_attribute = np.random.randint(0, 2, 1000)\n",
        "\n",
        "    metrics = [\n",
        "        (\"Demographic Parity Difference\", demographic_parity_difference(y_pred, protected_attribute)),\n",
        "        (\"Equal Opportunity Difference\", equal_opportunity_difference(y_true, y_pred, protected_attribute)),\n",
        "        (\"Disparate Impact Ratio\", disparate_impact_ratio(y_pred, protected_attribute))\n",
        "    ]\n",
        "\n",
        "    for metric_name, results in metrics:\n",
        "        print(f\"\\n{metric_name} Results:\")\n",
        "        for key, value in results.items():\n",
        "            if isinstance(value, float):\n",
        "                print(f\"{key}: {value:.6f}\")\n",
        "            else:\n",
        "                print(f\"{key}: {value}\")"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "SGS75aBFPfDr",
        "outputId": "6ee4f134-830a-458d-e425-a68fc7b49d45"
      },
      "execution_count": 4,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "\n",
            "Demographic Parity Difference Results:\n",
            "demographic_parity_difference: 0.001896\n",
            "prob_positive_unprivileged: 0.473054\n",
            "prob_positive_privileged: 0.474950\n",
            "reason_if_undefined: None\n",
            "\n",
            "Equal Opportunity Difference Results:\n",
            "equal_opportunity_difference: 0.000923\n",
            "tpr_unprivileged: 0.476000\n",
            "tpr_privileged: 0.476923\n",
            "reason_if_undefined: None\n",
            "\n",
            "Disparate Impact Ratio Results:\n",
            "disparate_impact_ratio: 0.996008\n",
            "prob_positive_unprivileged: 0.473054\n",
            "prob_positive_privileged: 0.474950\n",
            "reason_if_undefined: None\n"
          ]
        }
      ]
    }
  ]
}